/**
 * #info
 *
 *@author   wuwg
 * @version 2018.01.01
 *@createTime   2018-03-21
 *@updateTime   2018-04-12
 *@description  module  global
 */
/**
 * blur focus focusin focusout resize scroll click dblclick " +
 * mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
 * change select submit keydown keypress keyup contextmenu
 *
 * click dblclick mousedown mouseup  mouseenter mouseleave
 */

// https://github.com/axios/axios

/*
 * request
 * config  options
 * {
 * // `url` is the server URL that will be used for the request
 * url: '/user',
 *
 * // `method` is the request method to be used when making the request
 * method: 'get', // default
 *
 * // `baseURL` will be prepended to `url` unless `url` is absolute.
 * // It can be convenient to set `baseURL` for an instance of axios to pass relative URLs
 * // to methods of that instance.
 * baseURL: 'https://some-domain.com/api/',
 *
 * // `transformRequest` allows changes to the request data before it is sent to the server
 * // This is only applicable for request methods 'PUT', 'POST', and 'PATCH'
 * // The last function in the array must return a string or an instance of Buffer, ArrayBuffer,
 * // FormData or Stream
 * // You may modify the headers object.
 * transformRequest: [function (data, headers) {
 * // Do whatever you want to transform the data
 *
 * return data;
 * }],
 *
 * // `transformResponse` allows changes to the response data to be made before
 * // it is passed to then/catch
 * transformResponse: [function (data) {
 * // Do whatever you want to transform the data
 *
 * return data;
 * }],
 *
 * // `headers` are custom headers to be sent
 * headers: {'X-Requested-With': 'XMLHttpRequest'},
 *
 * // `params` are the URL parameters to be sent with the request
 * // Must be a plain object or a URLSearchParams object
 * params: {
 * ID: 12345
 * },
 *
 * // `paramsSerializer` is an optional function in charge of serializing `params`
 * // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
 * paramsSerializer: function(params) {
 * return Qs.stringify(params, {arrayFormat: 'brackets'})
 * },
 *
 * // `data` is the data to be sent as the request body
 * // Only applicable for request methods 'PUT', 'POST', and 'PATCH'
 * // When no `transformRequest` is set, must be of one of the following types:
 * // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
 * // - Browser only: FormData, File, Blob
 * // - Node only: Stream, Buffer
 * data: {
 * firstName: 'Fred'
 * },
 *
 * // `timeout` specifies the number of milliseconds before the request times out.
 * // If the request takes longer than `timeout`, the request will be aborted.
 * timeout: 1000,
 *
 * // `withCredentials` indicates whether or not cross-site Access-Control requests
 * // should be made using credentials
 * withCredentials: false, // default
 *
 * // `adapter` allows custom handling of requests which makes testing easier.
 * // Return a promise and supply a valid response (see lib/adapters/README.md).
 * adapter: function (config) {
 * /!* ... *!/
 * },
 *
 * // `auth` indicates that HTTP Basic auth should be used, and supplies credentials.
 * // This will set an `Authorization` header, overwriting any existing
 * // `Authorization` custom headers you have set using `headers`.
 * auth: {
 * username: 'janedoe',
 * password: 's00pers3cret'
 * },
 *
 * // `responseType` indicates the type of data that the server will respond with
 * // options are 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
 * responseType: 'json', // default
 *
 * // `responseEncoding` indicates encoding to use for decoding responses
 * // Note: Ignored for `responseType` of 'stream' or client-side requests
 * responseEncoding: 'utf8', // default
 *
 * // `xsrfCookieName` is the name of the cookie to use as a value for xsrf token
 * xsrfCookieName: 'XSRF-TOKEN', // default
 *
 * // `xsrfHeaderName` is the name of the http header that carries the xsrf token value
 * xsrfHeaderName: 'X-XSRF-TOKEN', // default
 *
 * // `onUploadProgress` allows handling of progress events for uploads
 * onUploadProgress: function (progressEvent) {
 * // Do whatever you want with the native progress event
 * },
 *
 * // `onDownloadProgress` allows handling of progress events for downloads
 * onDownloadProgress: function (progressEvent) {
 * // Do whatever you want with the native progress event
 * },
 *
 * // `maxContentLength` defines the max size of the http response content in bytes allowed
 * maxContentLength: 2000,
 *
 * // `validateStatus` defines whether to resolve or reject the promise for a given
 * // HTTP response status code. If `validateStatus` returns `true` (or is set to `null`
 * // or `undefined`), the promise will be resolved; otherwise, the promise will be
 * // rejected.
 * validateStatus: function (status) {
 * return status >= 200 && status < 300; // default
 * },
 *
 * // `maxRedirects` defines the maximum number of redirects to follow in node.js.
 * // If set to 0, no redirects will be followed.
 * maxRedirects: 5, // default
 *
 * // `socketPath` defines a UNIX Socket to be used in node.js.
 * // e.g. '/var/run/docker.sock' to send requests to the docker daemon.
 * // Only either `socketPath` or `proxy` can be specified.
 * // If both are specified, `socketPath` is used.
 * socketPath: null, // default
 *
 * // `httpAgent` and `httpsAgent` define a custom agent to be used when performing http
 * // and https requests, respectively, in node.js. This allows options to be added like
 * // `keepAlive` that are not enabled by default.
 * httpAgent: new http.Agent({ keepAlive: true }),
 * httpsAgent: new https.Agent({ keepAlive: true }),
 *
 * // 'proxy' defines the hostname and port of the proxy server
 * // Use `false` to disable proxies, ignoring environment variables.
 * // `auth` indicates that HTTP Basic auth should be used to connect to the proxy, and
 * // supplies credentials.
 * // This will set an `Proxy-Authorization` header, overwriting any existing
 * // `Proxy-Authorization` custom headers you have set using `headers`.
 * proxy: {
 * host: '127.0.0.1',
 * port: 9000,
 * auth: {
 * username: 'mikeymike',
 * password: 'rapunz3l'
 * }
 * },
 *
 * // `cancelToken` specifies a cancel token that can be used to cancel the request
 * // (see Cancellation section below for details)
 * cancelToken: new CancelToken(function (cancel) {
 * })
 *}
 */

/**
 *
 *
 *
 * axios.request(config)
 *
 * axios.get(url[, config])
 *
 * axios.delete(url[, config])
 *
 * axios.head(url[, config])
 *
 * axios.options(url[, config])
 *
 * axios.post(url[, data[, config]])
 *
 * axios.put(url[, data[, config]])
 *
 * axios.patch(url[, data[, config]])
 *
 * axios.all(iterable)
 *
 * axios.spread(callback)
 *
 *
 */
/**
 * Response
 * {
 * // `data` is the response that was provided by the server
 * data: {},
 *
 * // `status` is the HTTP status code from the server response
 * status: 200,
 *
 * // `statusText` is the HTTP status message from the server response
 * statusText: 'OK',
 *
 * // `headers` the headers that the server responded with
 * // All header names are lower cased
 * headers: {},
 *
 * // `config` is the config that was provided to `axios` for the request
 * config: {},
 *
 * // `request` is the request that generated this response
 * // It is the last ClientRequest instance in node.js (in redirects)
 * // and an XMLHttpRequest instance the browser
 * request: {}
 * }
 *
 * //https://www.jianshu.com/p/df464b26ae58 (中文文档)
 * // https://www.kancloud.cn/yunye/axios/234845   (中文文档)
 * axios.get('/user/12345')
 * .then(function(response) {
 * console.log(response.data);
 * console.log(response.status);
 * console.log(response.statusText);
 * console.log(response.headers);
 * console.log(response.config);
 * });
 */

/*
 *    import  es6Promise  from 'es6-promise';
 * // 如果不支持注册 promise
 * if(!window.Promise){
 * es6Promise.polyfill();
 *}
 */

// 如果不支持注册 promise --入口已经有polyfill了
/*
 * if(!window.Promise){
 * // https://github.com/stefanpenner/es6-promise
 * require('es6-promise').polyfill();
 *}
 */
import message from '../components/notice/message/index.js'
import notice from '../components/notice/notice/index.js'
import spin from '../components/tools/spin/index.js'
import loadingBar from '../components/tools/loading-bar/index.js'
import modal from '../components/popup/modal/index.js'
import _axios from 'axios'
import $http from './http'
import qs from 'qs'
import { name, version } from '../../../package.json'
import {formatDate} from "../components/form/date-picker/util";

const isServer = typeof window === 'undefined'

// 给原生 Object 上定义 JSON方法
/*
 * Object.prototype.toJSON= function () {
 * return  JSON.stringify(this);
 *};
 */
function ArteryConstructor () {
  /**
   * Artery类，一个全局变量.
   * @constructor ArteryConstructor
   */
  this.version = version
  this.author = 'wuwg'
  this.name = name
  // ajax
  this.ajax = $http
  // axios 原生的不做修改
  this.axios = _axios
  this.qs = qs
  this.isServer = isServer
}

ArteryConstructor.prototype = {
  /**
   * @see src\components\components\notice\message\index.js
   * @description
   * inherit  message
   *  另外Vue原型中也包含此message;
   *      Vue.prototype.$Message=message; 通过  this.$Message.info来调用
   *      通过直接调用以下方法来使用组件：
   * Artery.message.info(config)
   * Artery.message.success(config)
   * Artery.message.warning(config)
   * Artery.message.error(config)
   * Artery.message.loading(config)
   *
   * config配置
   * 属性         描述                                  类型          默认值
   * content    提示内容                                    String         -
   * render    自定义描述内容，使用 Vue 的 Render 函数    Function      -
   * duration    自动关闭的延时，单位秒，不关闭可以写 0        Number         1.5
   * onClose    关闭时的回调                                Function      -
   * closable    是否显示关闭按钮                          Boolean         false
   */
  message: message,
  /**
   * @see src\components\components\notice\notice\index.js
   * @description
   * inherit  notice
   *  另外Vue原型中也包含此notice;
   *      Vue.prototype.$Notice=notice; 通过  this.$Notice.info来调用
   *      通过直接调用以下方法来使用组件：
   * Artery.notice.info(config)
   * Artery.notice.success(config)
   * Artery.notice.warning(config)
   * Artery.notice.error(config)
   * Artery.notice.loading(config)
   *
   * config配置
   * 属性         描述                                  类型          默认值
   * title        通知提醒的标题                            String         -
   * desc        通知提醒的内容，为空或不填时，
   * 自动应用仅标题模式下的样式                    String         -
   * render    自定义描述内容，使用 Vue 的 Render 函数，
   * 如果同时设置了 render 和 desc，
   * 则只显示 render 的内容                     Function      -
   * duration    自动关闭的延时，单位秒，不关闭可以写 0        Number         4.5
   * onClose    关闭时的回调                                Function     -
   * name        当前通知的唯一标识                        String     自动
   */
  notice: notice,
  /**
   * @see src\components\components\tools\spin\index.js
   * @description
   * inherit  spin
   *  另外Vue原型中也包含此spin;
   *      Vue.prototype.$Spin=spin; 通过  this.$Spin来调用
   *      通过直接调用以下方法来使用组件：
   * Artery.spin.show(config)
   * Artery.spin.hide()
   * config 配置
   *
   * 属性         描述                                   类型          默认值
   * render    自定义描述内容，使用 Vue 的 Render 函数，  function      -
   * 如果同时设置了 render 和 desc，
   */
  spin: spin,
  /**
   * @see src\components\components\tools\loading-bar\index.js
   * @description
   * inherit  loadingBar
   *  另外Vue原型中也包含此loadingBar;
   *      Vue.prototype.$LoadingBar=loadingBar; 通过  this.$LoadingBar来调用
   *      通过直接调用以下方法来使用组件：
   * Artery.loadingBar.start()    //开始从 0 显示进度条，并自动加载进度
   * Artery.loadingBar.finish()   //结束进度条，自动补全剩余进度
   * Artery.loadingBar.error()    //以错误的类型结束进度条，自动补全剩余进度
   * Artery.loadingBar.update(percent)  //精确加载到指定的进度
   * Artery.loadingBar.config(options) //全局配置
   * Artery.loadingBar.config({
   * color: '#5cb85c',
   * failedColor: '#f0ad4e',
   * height: 5
   * });
   * Artery.loadingBar.destroy() //全局销毁
   *
   * options 配置
   * 属性             描述                                              类型          默认值
   * color         进度条的颜色，默认为 Artery 主色                     String        primary
   * failedColor   失败时的进度条颜色，默认为 Artery 出错时的颜色        String        error
   * height        进度条高度，单位 px                                 Number        2
   *
   */
  loadingBar: loadingBar,

  modal: modal,
  /**
   *
   * @name   consoleLog
   * @time 2018-03-21
   * @param showLog   {boolean}   是否输出日志
   * @param logInfo    {null}    要输出的日志信息
   * @description  简化输出日志的方法
   */
  consoleLog (showLog, logInfo) {
    if (showLog) {
      console.log(logInfo)
    }
  },
  /**
   *
   * @name   isOneOf
   * @time   2018-03-21
   * @param value       {boolean}    要检测的值
   * @param validList    {Array}    目标数组
   * @description  判断参数是否是其中之一
   */
  isOneOf (value, validList) {
    for (let i = 0; i < validList.length; i++) {
      if (value === validList[i]) {
        return true
      }
    }
    return false
  },
  /**
   *
   * @name   type
   * @time   2018-03-21
   * @param  obj       {object}    要检测的对象
   * @description  判断数据类型类型判断
   */
  type (obj) {
    const toString = Object.prototype.toString
    const map = {
      '[object Boolean]': 'boolean',
      '[object Number]': 'number',
      '[object String]': 'string',
      '[object Function]': 'function',
      '[object Array]': 'array',
      '[object Date]': 'date',
      '[object RegExp]': 'regExp',
      '[object Undefined]': 'undefined',
      '[object Null]': 'null',
      '[object Object]': 'object',
      '[object File]': 'file'
    }
    return map[toString.call(obj)]
  },
  /**
   *
   * @name   deepCopy
   * @time   2018-03-21
   * @param  data       {object}    需要复制的对象
   * @description  深度复制
   */
  deepCopy (data) {
    const t = this.type(data)
    let o
    if (t === 'array') {
      o = []
    } else if (t === 'object') {
      o = {}
    } else {
      return data
    }
    if (t === 'array') {
      for (let i = 0; i < data.length; i++) {
        o.push(this.deepCopy(data[i]))
      }
    } else if (t === 'object') {
      /*
       * Object.entries方法返回一个数组，成员是参数对象自身的（不含继承的）
       * 所有可遍历（enumerable）属性的键值对数组。
       */
      for (const [key, value] of Object.entries(data)) {
        o[key] = this.deepCopy(value)
      }
    }
    return o
  },
  /**
   *
   * @name   findComponentUpward
   * @time   2018-05-28
   * @param  context       {object}    上下文，当前的vm对象
   * @param  componentName       {String}    需要查找的组件名
   * @param  componentNames       {Array}   需要查找的组件名组
   * @returns {{}|*}   {object}       需要查找的组件对象
   * @description     向上查找组件，相当于jquery中的closet()
   */
  findComponentUpward (context, componentName, componentNames) {
    // 组件名称
    componentNames = typeof componentName === 'string' ? [componentName] : componentName
    // 父组件
    let parent = context.$parent
    //  父组件名称
    let name = parent.$options.name
    // 依次往上查找父级组件，直到找到符合条件的父组件
    while (parent && (!name || componentNames.indexOf(name) < 0)) {
      // 父组件
      parent = parent.$parent
      if (parent) {
        // 父组件名称
        name = parent.$options.name
      }
    }
    // 返回符合条件的父组件
    return parent
  },
  /**
   *
   * @name   findComponentsUpward
   * @time   2018-05-28
   * @param  context              {object}    上下文，当前的vm对象
   * @param  componentName       {String}     需要查找的组件名
   * @returns []   {Array}       需要查找的组件对象
   * @description     向上查找组件，相当于jquery中的  parents()
   */
  findComponentsUpward (context, componentName) {
    const parents = []
    const parent = context.$parent
    if (parent) {
      if (parent.$options.name === componentName) {
        parents.push(parent)
      }
      return parents.concat(this.findComponentsUpward(parent, componentName))
    } else {
      return []
    }
  },

  /**
   *
   * @name   findComponentDownward
   * @time   2018-05-28
   * @param  context       {object}    上下文，当前的vm对象
   * @param  componentName       {String}    需要查找的组件名
   * @returns {{}|*}   {object}       需要查找的组件对象
   * @description     向下查找某个组件，只要找到一个就会返回，原理类Array.every()方法
   */
  findComponentDownward (context, componentName) {
    // 所有的子组件
    const childrens = context.$children
    // 符合条件的子组件
    let children = null

    if (childrens.length) {
      for (const child of childrens) {
        const name = child.$options.name
        // 当前组件等于需要插件的子组件名称，找到一个即可
        if (name === componentName) {
          children = child
          break
        } else {
          /*
           * 继续查找
           * children = arguments.callee(child, componentName); //  严格模式下是禁止使用arguments.callee的。
           */
          children = this.findComponentDownward(child, componentName)
          if (children) {
            break
          }
        }
      }
    }
    return children
  },

  /**
   *
   * @name   findComponentsDownward
   * @time   2018-05-28
   * @param  context       {object}    上下文，当前的vm对象
   * @param  componentName       {String}    需要查找的组件名
   * @returns []   {Array}       需要查找的组件对象
   * @description     向下查找所有后代组件，相当于jquery中的find()
   */
  findComponentsDownward: function (context, componentName) {
    var _this = this
    return context.$children.reduce((components, child) => {
      if (child.$options.name === componentName) {
        components.push(child)
      }
      const foundChilds = _this.findComponentsDownward(child, componentName)
      return components.concat(foundChilds)
    }, [])
  },
  /**
   *
   * @name   findBrothersComponents
   * @time   2018-05-28
   * @param  context       {object}    上下文，当前的vm对象
   * @param  componentName       {String}    需要查找的组件名
   * @returns []   {Array}       需要查找的组件对象
   * @description     查找兄弟组件，相当于jquery中的siblings()
   */
  findBrothersComponents (context, componentName) {
    const res = context.$parent.$children.filter(item => {
      return item.$options.name === componentName
    })
    const index = res.indexOf(context)
    res.splice(index, 1)
    return res
  },

  //  防反跳;
  debounce (fn, _this, delay) {
    let waiting
    const later = function (_arguments) {
      fn.apply(_this, _arguments)
      waiting = false
    }
    return function () {
      const _arguments = arguments
      if (waiting) {
        clearTimeout(waiting)
      }
      // 执行回调函数
      waiting = setTimeout(() => {
        later(_arguments)
      }, typeof delay === 'number' ? delay : 50)
    }
  },
  /**
   *
   * @name   camelCase
   * @time   2018-05-28
   * @param  name       {String}   需要转的名称
   * @returns name  {String}       转换后的驼峰命名
   * @description    将传进去的名字转驼峰命名
   */
  camelCase (name) {
    const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g
    const MOZ_HACK_REGEXP = /^moz([A-Z])/
    return name.replace(SPECIAL_CHARS_REGEXP, function (_, separator, letter, offset) {
      return offset ? letter.toUpperCase() : letter
    }).replace(MOZ_HACK_REGEXP, 'Moz$1')
  },
  /**
   *
   * @name   getStyle
   * @time   2018-05-28
   * @param  element       {Object}   元素名称
   * @param  styleName     {String}   样式名称
   * @returns value      {String}     样式名称对应的值
   * @description   获取元素的某个样式名称对应的值
   */
  getStyle (element, styleName) {
    /*
     *  if (!element || !styleName) {
     * return null;
     * }
     * styleName = this.camelCase(styleName);
     * if (styleName === 'float') {
     * styleName = 'cssFloat';
     * }
     * try {
     * const computed = document.defaultView.getComputedStyle(element, '');
     * return element.style[styleName] || computed ? computed[styleName] : null;
     * } catch(e) {
     * return element.style[styleName];
     *}
     */
    // debugger
    if (!element || !styleName) {
      return null
    }
    const _styleName = styleName
    let _computed = null
    /*
     * jQuery源代码，会发现，其css()方法实现不是使用的
     * window.getComputedStyle
     * 而是document.defaultView.getComputedStyle，
     * 语法如下：
     * var style = window.getComputedStyle("元素", "伪类");
     * style.getPropertyValue(styleName) // 不支持 驼峰(w3c)
     * style.getAttribute(styleName) //  ie6 需要驼峰写法，但是不需要再考虑ie6了 (<ie9)，
     * ie
     * if(element.currentStyle){ // 经项目测试这种写法有在IE下获取的是100%，出现bug，document.defaultView.getComputedStyle IE9+可以使用
     *     _computed=element.currentStyle;
     *     let width = _computed.getAttribute(_styleName);
     *     return  _computed.getAttribute(_styleName);
     * }else {
     */
    _computed = document.defaultView.getComputedStyle(element, '')
    return _computed.getPropertyValue(_styleName)
    // }
  },

  /**
   *
   * @name   setStyle
   * @time   2018-06-8
   * @param  element       {Object}   元素名称
   * @param  styleName     {String}   样式名称
   * @param value          {String}   样式名称对应的值
   * @description   设置元素的某个样式名称对应的值
   */
  setStyle (element, styleName, value) {
    if (!element || !styleName) {
      return null
    }
    let _styleName = styleName
    if (typeof _styleName === 'object') {
      for (var prop in styleName) {
        if (_styleName.hasOwnProperty(prop)) {
          this.setStyle(element, prop, _styleName[prop])
        }
      }
    } else {
      _styleName = this.camelCase(_styleName)
      element.style[_styleName] = value
    }
  },
  /**
   *
   * @name   getPropByPath
   * @time   2018-05-28
   * @param  obj       {Object}   目标对象
   * @param  path      {String}   路径
   * @returns {}         {Object}
   * @description     传入对象，和路径，返回该路径所在的对象，key，和对应的value
   */
  getPropByPath (obj, path) {
    let tempObj = obj
    //
    path = path.replace(/\[(\w+)\]/g, '.$1')
    path = path.replace(/^\./, '')

    const keyArr = path.split('.')
    let i = 0

    for (let len = keyArr.length; i < len - 1; ++i) {
      const key = keyArr[i]
      if (key in tempObj) {
        tempObj = tempObj[key]
      } else {
        throw new Error('[Artery warn]: please transfer a valid prop path to form item!')
      }
    }
    return {
      o: tempObj, //   当前的key值所在的对象
      k: keyArr[i], // 当前的key值
      v: tempObj[keyArr[i]] // 当前的value
    }
  },
  /**
   *
   * @name   isMergeableObject
   * @time   2018-05-28
   * @returns function        {function}    返回一个函数，function(value){}，value为需要检测的对象
   * @description      判断是否是可合并对象，目标为 null  ||  RegExp  || Date对象时不可合并
   */
  isMergeableObject: function () {
    //  非null 判断
    function isNonNullObject (value) {
      return !!value && typeof value === 'object'
    }

    // 特殊对象判断
    function isSpecial (value) {
      var stringValue = Object.prototype.toString.call(value)

      return stringValue === '[object RegExp]' || stringValue === '[object Date]'
    }

    return function (value) {
      return isNonNullObject(value) && !isSpecial(value)
    }
  },

  /**
   * @name   deepmerge
   * @time   2018-05-28
   * @param  target  {object}    合并后的对象
   * @param  source  ｛object｝   需要合并的对象
   * @param  options ｛object｝   参数
   * @returns {*}
   * @description  深度合并对象
   * 详情参考：https://github.com/KyleAMathews/deepmerge/blob/master/index.js
   */
  deepmerge: function (target, source, options) {
    var options = null
    var _this = this

    // 默认参数。判断是否是可合并对象的方法
    var defaultIsMergeableObject = this.isMergeableObject()

    // 清空对象
    function emptyTarget (val) {
      // 如果是数组，那么就是 [] ||  {}
      return Array.isArray(val) ? [] : {}
    }

    //  除非另有规定，否则克隆
    function cloneUnlessOtherwiseSpecified (value, options) {
      return (options.clone !== false && options.isMergeableObject(value))
        ? deepmerge(emptyTarget(value), value, options)
        : value
    }

    // 合并数组的方法
    function defaultArrayMerge (target, source, options) {
      return target.concat(source).map(function (element) {
        //  除非另有规定，否则克隆
        return cloneUnlessOtherwiseSpecified(element, options)
      })
    }

    // 合并对象的方法
    function mergeObject (target, source, options) {
      // 目标
      var destination = {}
      // 判断对象是否可以合并
      if (options.isMergeableObject(target)) {
        // d循环对象
        Object.keys(target).forEach(function (key) {
          destination[key] = cloneUnlessOtherwiseSpecified(target[key], options)
        })
      }

      Object.keys(source).forEach(function (key) {
        if (!options.isMergeableObject(source[key]) || !target[key]) {
          destination[key] = cloneUnlessOtherwiseSpecified(source[key], options)
        } else {
          destination[key] = deepmerge(target[key], source[key], options)
        }
      })
      return destination
    }

    //  深度合并
    function deepmerge (target, source, options) {
      options = options || {}
      //  数组合并的方法
      options.arrayMerge = options.arrayMerge || defaultArrayMerge
      // 是否是可合并的对象
      options.isMergeableObject = options.isMergeableObject || defaultIsMergeableObject
      // 要合并的对象是否是数组
      var sourceIsArray = Array.isArray(source)
      // 目标对象树是数组
      var targetIsArray = Array.isArray(target)
      // 判断目标对象和要合并的对象是否是同一类型
      var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray
      // 如果不是同类型。
      if (!sourceAndTargetTypesMatch) {
        //  除非另有规定，否则克隆
        return cloneUnlessOtherwiseSpecified(source, options)
      } else if (sourceIsArray) {
        // 合并数组
        return options.arrayMerge(target, source, options)
      } else {
        // 合并对象
        return mergeObject(target, source, options)
      }
    }

    // 函数本身就是对象,函数一等公民，可以任意添加属性
    this.deepmerge.all = function deepmergeAll (array, options) {
      if (!Array.isArray(array)) {
        throw new Error('first argument should be an array')
      }

      return array.reduce(function (prev, next) {
        return deepmerge(prev, next, options)
      }, {})
    }
    // 自动执行
    return deepmerge(target, source, options)
  },

  /**
   *
   * @name   merge
   * @time   2018-05-28
   * @returns  traget        {object}   合并后的对象
   * @description      对象浅合并
   */
  merge (target) {
    for (let i = 1, j = arguments.length; i < j; i++) {
      const source = arguments[i] || {}
      for (const prop in source) {
        //  判断自身属性，如果有才添加，不判断原型
        if (source.hasOwnProperty(prop)) {
          const value = source[prop]
          if (value !== undefined) {
            target[prop] = value
          }
        }
      }
    }
    return target
  },
  /**
   *
   * @name   firstUpperCase
   * @time   2018-05-28
   * @param   str {String}
   * @returns  str        {String}   首字母大写后的值
   * @description      首字母大写
   */
  firstUpperCase (str) {
    return str.toString()[0].toUpperCase() + str.toString().slice(1)
  },
  /**
   *
   * @name   scrollTop
   * @time   2018-05-28
   * @param   el {object}     目标元素
   * @param   from {Number}    开始的位置
   * @param   to   {Number}    结束的的位置
   * @param   duration   {Number}    动画持续时间
   * @description      首字母大写
   */
  scrollTop (el, from = 0, to, duration = 500) {
    if (!window.requestAnimationFrame) {
      window.requestAnimationFrame = (
        window.webkitRequestAnimationFrame ||
                window.mozRequestAnimationFrame ||
                window.msRequestAnimationFrame ||
                function (callback) {
                  return window.setTimeout(callback, 1000 / 60)
                }
      )
    }

    const difference = Math.abs(from - to)
    /*
     * 后面乘以50，仅仅为了让duration这个时间显示出来的效果，更符合我们期望的毫秒数，
     * 50毫秒，时长为500毫秒感觉不合适
     */
    const step = Math.ceil(difference / duration * 50)

    function scroll (start, end, step) {
      if (start === end) return

      let d = (start + step > end) ? end : start + step
      if (start > end) {
        d = (start - step < end) ? end : start - step
      }

      if (el === window) {
        window.scrollTo(d, d)
      } else {
        el.scrollTop = d
      }
      window.requestAnimationFrame(() => scroll(d, end, step))
    }

    scroll(from, to, step)
  },
  /**
   *
   * @name   isEmptyArray
   * @time   2018-05-28
   * @param   val     {Array}     需要判断的值
   * @returns  ｛boolean｝        {boolean}
   * @description      判断是否是空数
   */
  isEmptyArray (val) {
    return val.reduce((isEmpty, str) => isEmpty && !str || (typeof str === 'string' && str.trim() === ''), true)
  },

  /**
   *
   * @name   hasClass
   * @time   2018-05-28
   * @param   el     {object}     目标元素
   * @param   cls     {className}     需要检测的class
   * @returns  ｛boolean｝      {boolean}
   * @description      判断目标元素是否包含某个class
   */
  hasClass (el, cls) {
    if (!el || !cls) {
      return false
    }
    if (cls.indexOf(' ') !== -1) {
      throw new Error('className should not contain space.')
    }
    if (el.classList) {
      return el.classList.contains(cls)
    } else {
      return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1
    }
  },
  /**
   *
   * @name   addClass
   * @time   2018-05-28
   * @param   el     {object}     目标元素
   * @param   cls     {className}     需要添加的class
   * @description      给目标元素添加某个class
   */
  addClass (el, cls) {
    if (!el) return
    var curClass = el.className
    var classes = (cls || '').split(' ')

    for (var i = 0, j = classes.length; i < j; i++) {
      var clsName = classes[i]
      if (!clsName) continue

      if (el.classList) {
        el.classList.add(clsName)
      } else if (!this.hasClass(el, clsName)) {
        curClass += ' ' + clsName
      }
    }
    if (!el.classList) {
      el.className = curClass
    }
  },
  /**
   *
   * @name   removeClass
   * @time   2018-05-28
   * @param   el     {object}     目标元素
   * @param   cls     {className}     需要移除的class
   * @description      移除目标元素某个class
   */
  removeClass (el, cls) {
    if (!el || !cls) return
    var classes = cls.split(' ')
    var curClass = ' ' + el.className + ' '

    for (var i = 0, j = classes.length; i < j; i++) {
      var clsName = classes[i]
      if (!clsName) continue

      if (el.classList) {
        el.classList.remove(clsName)
      } else if (this.hasClass(el, clsName)) {
        curClass = curClass.replace(' ' + clsName + ' ', ' ')
      }
    }
    if (!el.classList) {
      // el.className = trim(curClass);
      el.className = curClass.trim()
    }
  },
  /**
   *
   * @name   getScrollBarSize
   * @time   2018-05-28
   * @returns  ｛boolean｝      {boolean}
   * @description      获取浏览器的滚动条宽度
   */
  getScrollBarSize: (function () {
    // 滚动条宽
    let barSize

    function countScrollBarSize () {
      const inner = document.createElement('div')
      inner.style.width = '100%'
      inner.style.height = '200px'

      const outer = document.createElement('div')
      const outerStyle = outer.style

      outerStyle.position = 'absolute'
      outerStyle.top = 0
      outerStyle.left = 0
      outerStyle.pointerEvents = 'none'
      outerStyle.visibility = 'hidden'
      outerStyle.width = '200px'
      outerStyle.height = '150px'
      outerStyle.overflow = 'hidden'

      outer.appendChild(inner)

      document.body.appendChild(outer)

      //  outer overflow = 'hidden'; 下获取容器宽
      const widthContained = inner.offsetWidth
      //  outer overflow = 'scroll'; 下获取容器宽
      outer.style.overflow = 'scroll'
      let widthScroll = inner.offsetWidth
      //
      if (widthContained === widthScroll) {
        widthScroll = outer.clientWidth
      }
      // 移除元素
      document.body.removeChild(outer)

      barSize = widthContained - widthScroll
    }

    return function (fresh) {
      if (fresh || barSize === undefined) {
        // 计算滚动条的宽
        countScrollBarSize()
      }
      return barSize
    }
  })(),

  /**
   *
   * @name   on
   * @time   2018-05-28
   * @param  element  {obejct}  需要绑定事件的对象或者元素
   * @param  event   ｛string｝ 需要绑定的事件名
   * @param  handler ｛function｝ 事件的回调函数
   * @description      为元素或对象绑定事件
   */
  on: (function () {
    if (isServer) {
      return
    }
    if (document.addEventListener) {
      return function (element, event, handler) {
        if (element && event && handler) {
          element.addEventListener(event, handler, false)
        }
      }
    } else {
      return function (element, event, handler) {
        if (element && event && handler) {
          element.attachEvent('on' + event, handler)
        }
      }
    }
  })(),
  /**
   *
   * @name   off
   * @time   2018-05-28
   * @param  element  {obejct}  需要解除事件的对象或者元素
   * @param  event   ｛string｝ 需要解除的事件名
   * @param  handler ｛function｝ 解除事件的回调函数
   * @description      为元素或对象解除事件
   */
  off: (function () {
    if (isServer) {
        return
    }
    if (document.removeEventListener) {
      return function (element, event, handler) {
        if (element && event) {
          element.removeEventListener(event, handler, false)
        }
      }
    } else {
      return function (element, event, handler) {
        if (element && event) {
          element.detachEvent('on' + event, handler)
        }
      }
    }
  })(),
  /**
   *
   * @name   once
   * @time   2018-06-08
   * @param  element  {obejct}  需要解除事件的对象或者元素
   * @param  event   ｛string｝ 需要解除的事件名
   * @param  handler ｛function｝ 解除事件的回调函数
   * @description      为元素或对象解除事件
   */
  once (element, event, handler) {
    var listener = function () {
      if (handler) {
        handler.apply(this, arguments)
      }
      this.off(element, event, listener)
    }
    this.on(element, event, listener)
  },
  /**
   * @name   isHidden
   * @time   2019-03-14
   * @param element {obejct}  目标元素
   * @returns {boolean} 元素是否隐藏
   * @description      判断元素是否隐藏
   */
  isHidden (element) {
    return !this.isVisible(element)
  },
  /**
   * @name   isVisible
   * @time   2019-03-14
   * @param element {obejct}  目标元素
   * @returns {boolean} 元素是否可见
   * @description      判断元素是否可见
   */
  isVisible (element) {
    return element.offsetWidth > 0 || element.offsetHeight > 0 || element.getClientRects().length > 0
  },
  /**
   *
   * @name   getScroll
   * @time   2018-05-28
   * @param  target  {object}  目标对象
   * @param  top    {Boolean}  水平还是垂直滚动条
   * @returns  ｛boolean｝      {boolean}
   * @description      获取当前滚动条的位置
   */
  getScroll (target, top) {
    const prop = top ? 'pageYOffset' : 'pageXOffset'
    const method = top ? 'scrollTop' : 'scrollLeft'
    // safari 比较特别，有自己获取scrollTop的函数, window.pageYOffset || window.pageXOffset ；
    let ret = target[prop]

    if (typeof ret !== 'number') {
      ret = window.document.documentElement[method]
    }
    return ret

    /*
     * 完美的获取scrollTop 赋值短语 ：
     * var scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
     */
  },

  /**
   *
   * @name   getOffset
   * @time   2018-05-28
   * @param  element  {object}  目标对象
   * @returns  ｛top:number,left:number｝      {object}
   * @description      获取距离页面的位置，左和上
   */
  getOffset (element) {
    const rect = element.getBoundingClientRect()
    const scrollTop = this.getScroll(window, true)
    const scrollLeft = this.getScroll(window)

    const docEl = window.document.body
    const clientTop = docEl.clientTop || 0
    const clientLeft = docEl.clientLeft || 0

    return {
      top: rect.top + scrollTop - clientTop,
      left: rect.left + scrollLeft - clientLeft
    }
  },
  /**
   * @name preventDefault
   * @param  event  {object}  event对象
   * @description 阻止默认事件
   */
  preventDefault (event) {
    if (event.preventDefault) {
      event.preventDefault()
    } else {
      event.returnValue = false
    }
  },
  /**
   * @name stopPropagation
   * @param  event  {object}  event对象
   * @description 阻止事件冒泡
   */
  stopPropagation (event) {
    if (event.stopPropagation) {
      event.stopPropagation()
    } else {
      event.cancelBubble = true
    }
  },
  /**
   * @name stopEvent
   * @param  event  {object}  event对象
   * @description 阻止事件等同于阻止默认事件+阻止事件冒泡
   */
  stopEvent (event) {
    this.preventDefault(event)
    this.stopPropagation(event)
  },

  /**
   * @name parseUrl
   * @returns  {}, {object}  返回 search后面所有参数
   * @description 解析url参数
   */
  parseUrl () {
    const url = window.location.search
    const obj = {}
    const reg = /[?&][^?&]+=[^?&]+/g
    const arr = url.match(reg)
    if (arr) {
      arr.forEach((item) => {
        const tempArr = item.substring(1).split('=')
        const key = decodeURIComponent(tempArr[0])
        const val = decodeURIComponent(tempArr[1])
        obj[key] = val
      })
    }
    return obj
  },
  delHTMLTag: (function () {
    // 代码优化，仅仅创建一个parttern 模式
    const _parttern = /(<.*?>)|(<\/*?>)/igm
    return function (str) {
      return str.replace(_parttern, '')
    }
  })(),
  /**
   *
   * @name   dateFormat
   * @time   2018-05-21
   * @param  formatter  {string}  日期、时间格式化字符串
   * @returns  string      {string}
   * @description   用户格式化日期，返回格式化后的字符串
   */
  dateFormat (value, formatter) {
    return formatDate(value, formatter)
  },

  MutationObserver: isServer ? false : (window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver || false),
  /**
   * @updateTime   2018-05-15
   * @name         setScrollBarPosition
   * @param   element  {dom}     目标元素
   * @scrollContain   element  {dom}     滚动条所在的容器元素
   * @description  设置滚动条的位置
   */
  setScrollBarPosition (element, scrollContain) {
    //  获取矩形容器
    const rectTarget = element.getBoundingClientRect()
    const rectContain = scrollContain.getBoundingClientRect()
    // debugger
    const bottomOverflowDistance = rectContain.bottom - rectTarget.bottom
    const topOverflowDistance = rectContain.top - rectTarget.top
    // bottomOverflowDistance小于0，说明目标元素不在可视范围之内
    if (bottomOverflowDistance < 0) {
      scrollContain.scrollTop += Math.abs(bottomOverflowDistance) + 2
    }
    // topOverflowDistance 大于0，说明目标元素不在可视范围之内
    if (topOverflowDistance > 0) {
      scrollContain.scrollTop -= (Math.abs(topOverflowDistance) + 2)
    }
  },
  loadPageData (url, queryInfo, params) {
    return this.requestPageData(url, queryInfo, params, false)
  },
  getPageData (url, queryInfo, params) {
    return this.requestPageData(url, queryInfo, params, true)
  },
  requestPageData (url, queryInfo, params, isGet) {
    if (params) {
      if (url.indexOf('?') > 0) {
        url += '&' + qs.stringify(params, { indices: false })
      } else {
        url += '?' + qs.stringify(params, { indices: false })
      }
    }
    // 保存自定义参数供导出excel用
    if (queryInfo) {
      const tableId = queryInfo.tableId
      if (tableId) {
        if (!this.loadPageInfo) {
          this.loadPageInfo = {}
        }
        this.loadPageInfo[tableId + '_customParams'] = Artery.deepCopy(params)
        this.loadPageInfo[tableId + '_url'] = url
      }
    }

    const config = isGet ? {
      headers: { 'X-HTTP-Method-Override': 'get' }
    } : {}

    return this.ajax.post(url, queryInfo, config)
  },
  /**
   *
   * @param codeType 代码类型
   * @param detail 是否返回详细数据
   * @returns {Promise}
   */
  loadCode (codeType, detail = false) {
    const _this = this
    return new Promise((resolve, reject) => {
      if (!codeType) {
        resolve([])
        return
      }
      let url = ''
      if (this.codeUrl) {
        url = this.codeUrl.replace("{codeType}", codeType)
      } else if (window.contextPath) {
        url = `${window.contextPath}/artery/code/list/${codeType}`
      } else {
        // 默认相对路径，表单开发有<base>标签会自动适配，前后端分离可在入口js中配置Artery.ajax.defaults.baseURL=‘http://xxx/’解决
        // 更好的路子是开发阶段配置devTools 将artery开头的请求都路由到‘http://xxx/’一个后台上，然后部署的时候通过反向代理直接适配
        url = `artery/code/list/${codeType}`
      }
      _this.ajax.get(url, {params: {codeType: codeType, valid: 'All'}}).then(function (result) {
        if (result) {
          resolve(detail ? result : result.map(c => {
            return {
              code: c.code,
              name: c.name
            }
          }))
        } else {
          resolve([])
        }
      }).catch(function (err) {
        console.log('加载单值代码出错！' + err.status)
        reject(err)
      });
    });
  },
  /**
   * @updateTime   2018-06-19
   * @name         resolve
   * @param          obj  {object}       请求地址
   * @param          emptyPojoObj  {object}       拥有全部所需key的object
   * @returns        obj {object}
   * @description  用户处理thyemleaf标签的结果，防止因为语法错误
   */
  resolveObj (obj, emptyPojoObj) {
    if (obj != undefined) return obj
    if (emptyPojoObj) return emptyPojoObj
    return null
  },
  open (config) {
    if (!config && !config.url) {
      throw new Error('参数不能为空');
    }
    if (this.type(config) === 'string') {
      config = {
        url: config
      }
    }
    let targetType = config.target || config.targetType || '_self'; // 添加config.target, A5一直叫target,保留一下习惯
    let url = config.url;
    let params = config.params || {};
    if (targetType === '_window') {
      params.frame = true;//是否从iframe发出的请求
    }
    //  拼接参数
    let firstParam = false;
    if (Object.keys(params).length > 0) {
      if (!url.includes("?")) {
        url += "?";
        firstParam = true;
      } else if (url.endsWith("?")) {
        firstParam = true;
      }
    }

    for (let name in params) {
      if (firstParam) {
        url += name + "=" + params[name];
        firstParam = false;
      } else {
        url += "&" + name + "=" + params[name];
      }
    }

    // 根据不同target类型打开不同窗口
    if (targetType === '_self' || targetType === '_blank') {
      let win = window.open(url, targetType);
      if (targetType === '_blank') {
        this.saveOpen(win);
      }
    } else if (targetType === '_window') {
      let url_ = encodeURI(url);

      // 公司业务模型一般都不能走浏览器缓存, 所以默认每次不缓存
      config.refresh = typeof(config.refresh) === 'boolean' ? config.refresh : true;

      let modInstance = null;
      if (config.refresh) {
        modInstance = modal.newInstance(config);
        url_ = url_ + (url_.includes("?") ?  "&" : "?") + "aty_ts=" + new Date().getTime();
      } else {
        this._modalInstance = this._modalInstance || {}
        let key = JSON.stringify(config)
        modInstance = this._modalInstance[key]
        if (!modInstance) {
          modInstance = modal.newInstance(config);
          this._modalInstance[key] = modInstance
        }
      }

      let mod = modInstance.component;
      mod.jsControlContent = true;
      mod.showUrl = url_;
      mod.width = config.width || 600;
      mod.iframeStyle = "width: 100%; height:" + (config.height || 300) + "px;";
      mod.maskClosable = false;
      mod.iframeId = "iframeid_" + new Date().getTime();
      if (config.onOk) {
        const okFun = mod.ok
        mod.ok = function () {
          okFun.call()
          config.onOk(mod.iframeId)
        }
      }
      if (config.cancel) {
        mod.cancel = config.cancel
      }
      if (config.onCancel) {
        const cancelFun = mod.cancel
        mod.cancel = function () {
          cancelFun.call()
          config.onCancel(mod.iframeId)
        }
      }
      if (config.refresh) {
        // 重写model组件的close方法
        let _this = this
        mod.close = function () {
          _this.openModals.pop()
          modInstance.remove()
        }
      }
      if (config.close) {
        mod.close = config.close
      }
      if (config.onClose) {
        const closeFun = mod.close
        mod.close = function () {
          closeFun.call()
          config.onClose(mod.iframeId)
        }
      }
      mod.open();
      this.saveOpen(mod);
      return mod;
    } else {
      document.getElementById(targetType).src = url;
    }
  },
  /**
   * 保存打开的窗口信息
   * @param win open方法打开的4种类型窗口
   */
  saveOpen (win) {
    // 保存打开的窗口信息
    if (!this.openModals) {
      this.openModals = []
    }
    this.openModals.push(win)
  },
  /**
   * 关闭窗口
   */
  close () {
    if (this.openModals && this.openModals.length > 0) {
      let modal = this.openModals[this.openModals.length - 1];
      modal.close()
    }
  },
  /**
   * 关闭所有窗口
   */
  closeAll () {
    if (this.openModals) {
      while (this.openModals.length > 0) {
        this.openModals.pop().close()
      }
    }
  },
  /**
   * 导出excel
   * @param url
   * @param queryInfo
   */
  exportExcel (table, queryInfo) {
    // 设置url
    const loadPageUrl = this.loadPageInfo ? this.loadPageInfo[table.tableId + '_url'] : null
    let url = table.expReqUrl || loadPageUrl
    if (!url) {
      this.message.error('导出请求url为空')
      return
    }
    // 如果没有自定义导出url则设置一个标识
    if (!table.expReqUrl || table.expReqUrl.length === 0) {
      url += (url.indexOf('?') > 0) ? '&reqExportData=true' : '?reqExportData=true'
    }
    // 设置导出标识
    queryInfo.exportData = true

    // 设置自定义参数
    const loadPageCustomParams = this.loadPageInfo ? this.loadPageInfo[table.tableId + '_customParams'] : null
    queryInfo.customParams = table.expParams || loadPageCustomParams

    // 显示进度条
    let progressInterval = null
    if (table.showExpProgress) {
      table.expProgressPercent = 0
      table.expProgressDisplay = 'block'
      queryInfo.expId = new Date().getTime()
      const _this = this
      // 轮训查询后台进度
      progressInterval = setInterval(function () {
        _this.ajax.get('artery/exportexcel/getProcess/' + queryInfo.expId).then(function (res) {
          table.expProgressPercent = res.percent
        }).catch(function (error) {
          console.log('fetch export progress error :' + error)
        })
      }, 1000)
    } else {
      this.spin.show({
        render: (h) => {
          return h('div', [
            h('aty-icon', {
              class: 'demo-spin-icon-load',
              props: {
                type: 'load-c',
                size: 18
              }
            }),
            h('div', '正在生成excel，请稍候！')
          ])
        }
      })
    }
    // 发送请求
    return _axios.post(url, queryInfo, { responseType: 'blob', timeout: 0 }).then(res => {
      if (table.showExpProgress) {
        table.expProgressDisplay = 'none'
        clearInterval(progressInterval)
      } else {
        this.spin.hide()
      }
      let fileType = res.headers['file-type'];
      const content = res.data
      const blob = new Blob([content])
      let expFileName = queryInfo.expFileName
      if (!expFileName || expFileName.length === 0) {
        expFileName = '导出文件'
      }
      const fileName = expFileName + '.' + fileType
      if ('download' in document.createElement('a')) {
        const downLink = document.createElement('a')
        downLink.download = fileName
        downLink.style.display = 'none'
        downLink.href = URL.createObjectURL(blob)
        document.body.appendChild(downLink)
        downLink.click()
        URL.revokeObjectURL(downLink.href)
        document.body.removeChild(downLink)
      } else {
        navigator.msSaveBlob(blob, fileName)
      }
    }).catch(error => {
      if (table.showExpProgress) {
        table.expProgressDisplay = 'none'
        clearInterval(progressInterval)
      } else {
        this.spin.hide()
      }
      console.log('export excel error : ' + error)
    })
  },
  uuidPart () {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
  },
  uuid () {
    return (this.uuidPart() + this.uuidPart() + '-' + this.uuidPart() + '-' + this.uuidPart() + '-' + this.uuidPart() + '-' + this.uuidPart() + this.uuidPart() + this.uuidPart())
  }
}

const module = new ArteryConstructor()
// 注册全局模板混入对象
if (isServer) {
  global.atyTplMixin = {}
} else {
window.atyTplMixin = {}
}
export default module
